from __future__ import division

import random

from concepts import *

from .building import *
from .path import *
from .person import *
from .terrain import *


class Map(Object):
    normal_level_m =_ ( 20) # meters from bottom to NN
    ground_level_m =_ (  4) # meters from NN to ground
    scale          =_ (100) # meters per rendering unit on the z-axis
    
    width      = Attribute()
    height     = Attribute()
    geography  = Attribute()
    city       = Attribute()
    
    path_map   = Attribute()
    street_map = Attribute()
    water_map  = Attribute()
    
    heightfield   = Attribute()
    fertility_map = Attribute()

    def setup(self):
        self.generate()
        
        self.path_map   = PathingMap(self.width, self.height)
        self.street_map = PathingMap(self.width, self.height)
        self.water_map  = PathingMap(self.width, self.height)
        
        self._tiles = [self.setup_tile(Tile(self, MapPosition(x, y, self)))
                       for y in range(self.height)
                       for x in range(self.width)]
    
    @property
    def water_level(self):
        return self.meters_nn(0)
    
    @property
    def ground_level(self):
        return self.meters_nn(self.ground_level_m)
    
    def meters_nn(self, meters):
        return (self.normal_level_m + meters) / self.scale
    
    def meters(self, meters):
        return meters / self.scale
    
    def in_meters_nn(self, value):
        return (value - self.water_level) * self.scale
    
    def generate(self):
        hf_dsq = DiamondSquare(scatter = self.meters(1))
        hf_dsq.add_stages(5, lambda x, y: self.meters_nn(self.geography.height(x / self.width, y / self.height)))
        
        fm_dsq = DiamondSquare(scatter = 0.05)
        fm_dsq.add_stages(6, lambda x, y: random.random() * 0.8 + 0.2)
        
        heightfield = hf_dsq.generate(self.width, self.height)
        
        # Note: Maps have Y the wrong way around!
        self.heightfield   = numpy.where(heightfield <= self.ground_level, heightfield, self.ground_level)        
        self.fertility_map = fm_dsq.generate(self.width, self.height)

    def update_tile(self, tile):
        self.path_map  .set_passable(tile, tile.is_passable)
        self.street_map.set_passable(tile, tile.is_road)
        self.water_map .set_passable(tile, tile.is_water)
    
    def setup_tile(self, tile):
        tile.meters_nn = round(self.in_meters_nn(self.heightfield[tile.x, self.height - tile.y - 1]), 2) 
        tile.fertility = self.fertility_map[tile.x, self.height - tile.y - 1]
        
        if tile.meters_nn < self.ground_level_m:
            tile.terrain = Sea
        elif tile.fertility > 0.75:
            tile.terrain = Forest
        else:
            tile.terrain = Grass
        
        return tile
    
    def add(self, building):
        for dx in range(building.size):
            for dy in range(building.size):
                self.at(building.position.in_direction(dx, dy)).building = building
    
    def at(self, x, y = None):
        if y is None:
            position = x
        else:
            position = MapPosition(x, y, self)
        
        if position._x < 0 or self._width <= position._x:
            return Tile(self, position)
        
        if position._y < 0 or self._height <= position._y:
            return Tile(self, position)
        
        if self._has(position._x, position._y):
            return self._at(position._x, position._y)
        else:
            return Tile(self, position)
    
    def _has(self, x, y):
        return 0 <= y * self._width + x < len(self._tiles)
    
    def _at(self, x, y):
        return self._tiles[y * self._width + x]
    
    def do(self, callback):
        for y in range(self.height):
            for x in range(self.width):
                callback(self._at(x, y))
    
    def day(self, day, month, year):
        self.do(lambda tile: tile.day(day, month, year))
    
    @property
    def tiles(self):
        return self._tiles[:]
    
    def __iter__(self):
        return iter(self.tiles)


class Tile(Object):
    source     = Attribute()
    position   = Attribute()
    area_sized = position._.extend_by
    
    meters_nn  = Attribute()
    fertility  = Attribute()
    
    def setup(self):
        self._building = None
    
    @property
    def x(self):
        return int(self._position._x)
    
    @property
    def y(self):
        return int(self._position._y)
    
    @property
    def terrain(self):
        return self._terrain
    
    @terrain.setter
    def terrain(self, terrain):
        if isinstance(terrain, type):
            self._terrain = terrain(self, self.position)
        else:
            self._terrain = terrain
        
        self.source.update_tile(self)
    
    @property
    def building(self):
        return self._building
    
    @building.setter
    def building(self, building):
        self._building = building
        self.source.update_tile(self)

    def in_direction(self, dx, dy = None):
        return self.source.at(self.position.in_direction(dx, dy))
    
    @property
    def has_a_building(self):
        return self._building is not None
    
    def has_building(self, type):
        return self.has_a_building and isinstance(self.building, type)
    
    def has_building_with(self, callback):
        return self.has_a_building and callback(self.building)
    
    def nearby(self, type, radius):
        all = self.source.city.index(type)
        
        return [stuff for stuff in all
                if isinstance(stuff, type)
                and stuff.position is not None
                and stuff.position.distance(self.position) <= radius]
    
    def random_nearby(self, type, radius):
        nearby = self.nearby(type, radius)
        
        if nearby:
            return random.choice(nearby)
        else:
            return None
    
    def closest(self, type, max_distance = None):
        all   = self.source.city.index(type)
        types = [stuff for stuff in all
                 if isinstance(stuff, type)
                 and stuff.position is not None]
        types.sort(key = lambda x: x.position.distance(self.position))
        
        if not types:
            return None
        
        if max_distance is not None and types[0].position.distance(self.position) > max_distance:
            return None
        
        return types[0]
    
    @property
    def neighbours(self):
        for dx in (-1, 0, 1):
            for dy in (-1, 0, 1):
                if dx == dy == 0:
                    continue
                
                tile = self.in_direction(dx, dy)
                
                if tile.is_in_bounds:
                    yield tile
    
    @property
    def is_passable(self):
        if not self.is_in_bounds:
            return False
        
        if self.building is None:
            return self.terrain.is_land
        else:
            return self.building.is_passable
    
    @property
    def is_road(self):
        if not self.is_in_bounds:
            return False
        
        return self.building is not None and self.building.is_road
        
    @property
    def is_walkable(self):
        if not self.is_in_bounds:
            return False
        
        return self.building is not None and self.building.is_walkable
    
    @property
    def is_water(self):
        if not self.is_in_bounds:
            return False
        
        return self.building is None and not self.terrain.is_land
    
    @property
    def is_next_to_water(self):
        if not self.is_in_bounds:
            return False
        
        for tile in self.neighbours:
            if tile.is_water:
                return True
        
        return False
    
    @property
    def is_in_bounds(self):
        return (0 <= self.position.x < self.source.width and
                0 <= self.position.y < self.source.height)
    
    def can_build(self, type):
        if not self.is_in_bounds:
            return False
        elif type.requires is None:
            return (self.building is None and
                    any(issubclass(self.terrain.type, type)
                        for type in type.terrain))
        else:
            return (self.building is not None and
                    issubclass(self.building.type, type.requires) and
                    any(issubclass(self.terrain.type, type)
                        for type in type.terrain))
    
    def path_to(self, target, streets = False, water = False):
        if target is None:
            return None
        
        if water:
            return Path(self.source, self.source.water_map,  self.position, target)
        elif streets:
            return Path(self.source, self.source.street_map, self.position, target)
        else:
            return Path(self.source, self.source.path_map,   self.position, target)
    
    @property
    @memoized
    def relative_position(self):
        return (self.x / self.source.width,
                self.y / self.source.height)

    def day(self, day, month, year):
        # self._terrain.day(day, month, year) FIXME
        pass


class MapPosition(Position):
    def __init__(self, x, y, map):
        super(MapPosition, self).__init__(x, y)
        
        self._map = map
    
    @property
    def inheritable_attributes(self):
        return {'map': self._map}
    
    def extend_by(self, width, height, continuous = False):
        return MapArea(self, width, height, continuous, self._map)
    
    def fit_building(self, type):
        return self.extend_by(type.size, type.size).fit_buildings(type)


class MapArea(Area):
    map = Attribute()
        
    @property
    def inheritable_attributes(self):
        return {'map': self.map}

    def fit_buildings(self, type):
        used = set()
        
        for position in self.positions:
            if position in used:
                continue
            
            building = type(city = self.map.city, position = position, virtual = True)
            
            if all(self.map.at(position).can_build(building) and position in self for check in building.area.positions):
                used |= set(building.area.positions)
                
                yield building
